package org.jhlabs.scany.engine.analysis.kr.ws;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.jhlabs.scany.engine.analysis.kr.dic.Dictionary;
import org.jhlabs.scany.engine.analysis.kr.ma.AnalysisOutput;
import org.jhlabs.scany.engine.analysis.kr.ma.CompoundEntry;
import org.jhlabs.scany.engine.analysis.kr.ma.MorphAnalyzer;
import org.jhlabs.scany.engine.analysis.kr.ma.MorphException;
import org.jhlabs.scany.engine.analysis.kr.ma.PatternConstants;
import org.jhlabs.scany.engine.analysis.kr.ma.WordEntry;
import org.jhlabs.scany.engine.analysis.kr.ma.rule.MorphRule;
import org.jhlabs.scany.engine.analysis.kr.ma.rule.SyllableRule;
import org.jhlabs.scany.engine.analysis.kr.ma.rule.VerbRule;

/**
 * 
 * @author smlee
 *
 */
public class WordSpaceAnalyzer {

	private MorphAnalyzer morphAnal;

	public WordSpaceAnalyzer() {
		morphAnal = new MorphAnalyzer();
		morphAnal.setExactCompound(false);
	}

	public List analyze(String input) throws MorphException {

		List stack = new ArrayList();

		WSOutput output = new WSOutput();

		int wStart = 0;

		int sgCount = -9;

		Map<Integer, Integer> fCounter = new HashMap();

		for(int i = 0; i < input.length(); i++) {

			char[] f = Dictionary.getSyllableFeature(input.charAt(i));

			String prefix = i == input.length() - 1 ? "X" : input.substring(wStart, i + 2);
			Iterator iter = Dictionary.findWithPrefix(prefix);

			List<AnalysisOutput> candidates = new ArrayList();

			WordEntry entry = null;

			if(input.charAt(i) == '있' || input.charAt(i) == '없' || input.charAt(i) == '앞') {
				addSingleWord(input.substring(wStart, i), candidates);

				// 다음 음절이 2음절 이상 단어에 포함되어 있고 마지막 음절이 아니라면   띄워쓰기 위치가 아닐 가능성이 크다.
				// 부사, 관형사, 감탄사 등 단일어일 가능성인 경우 띄워쓰기가 가능하나, 
				// 이 경우는 다음 음절을 조사하여 
			} else if(i != input.length() - 1 && iter.hasNext()) {
				// 아무짓도 하지 않음.
				sgCount = i;
			} else if(!iter.hasNext() && (entry = Dictionary.getBusa(input.substring(wStart, i + 1))) != null) {
				candidates.add(buildSingleOutput(entry));

				// 현 음절이 조사나 어미가 시작되는 음절일 가능성이 있다면...	
			} else if(f[SyllableRule.IDX_EOGAN] == '1' || f[SyllableRule.IDX_JOSA1] == '1') {
				if(f[SyllableRule.IDX_JOSA1] == '1')
					candidates.addAll(anlysisWithJosa(input.substring(wStart), i - wStart));

				if(f[SyllableRule.IDX_EOGAN] == '1')
					candidates.addAll(anlysisWithEomi(input.substring(wStart), i - wStart));
			}

			// 호보가 될 가능성이 높은 순으로 정렬한다.
			Collections.sort(candidates, new WSOuputComparator());

			// 길이가 가장 긴 단어를 단일어로 추가한다.
			appendSingleWord(candidates);

			// 분석에 실패한 단어를 
			analysisCompouns(candidates);

			// 호보가 될 가능성이 높은 순으로 정렬한다.
			Collections.sort(candidates, new WSOuputComparator());

			int reseult = validationAndAppend(output, candidates, input);
			if(reseult == 1) {
				i = output.getLastEnd() - 1;
				wStart = output.getLastEnd();
			} else if(reseult == -1) {
				Integer index = fCounter.get(output.getLastEnd());
				if(index == null)
					index = output.getLastEnd();
				else
					index = index + 1;
				i = index;
				wStart = output.getLastEnd();
				fCounter.put(output.getLastEnd(), index);
			}

		}

		// 분석에 실패하였다면 원래 문자열을 되돌려 준다.
		if(output.getLastEnd() < input.length()) {

			String source = input.substring(output.getLastEnd());
			int score = Dictionary.getWord(source) == null ? AnalysisOutput.SCORE_ANALYSIS
					: AnalysisOutput.SCORE_CORRECT;
			AnalysisOutput o = new AnalysisOutput(source, null, null, PatternConstants.POS_NOUN,
					PatternConstants.PTN_N, score);

			o.setSource(source);
			output.getPhrases().add(o);
			morphAnal.confirmCNoun(o);

		}

		return output.getPhrases();
	}

	/**
	 * 조사로 끝나는 어구를 분석한다.
	 * @param snipt
	 * @param js
	 * @return
	 * @throws MorphException
	 */
	private List anlysisWithJosa(String snipt, int js) throws MorphException {

		List<AnalysisOutput> candidates = new ArrayList();
		if(js < 1)
			return candidates;

		int jend = findJosaEnd(snipt, js);

		if(jend == -1)
			return candidates; // 타당한 조사가 아니라면...

		String input = snipt.substring(0, jend);

		boolean josaFlag = true;

		for(int i = input.length() - 1; i > 0; i--) {

			String stem = input.substring(0, i);

			String josa = input.substring(i);

			char[] feature = Dictionary.getSyllableFeature(josa.charAt(0));

			if(josaFlag && feature[SyllableRule.IDX_JOSA1] == '1') {
				morphAnal.analysisWithJosa(stem, josa, candidates);
			}

			if(josaFlag && feature[SyllableRule.IDX_JOSA2] == '0')
				josaFlag = false;

			if(!josaFlag)
				break;

		}

		if(input.length() == 1) {
			AnalysisOutput o = new AnalysisOutput(input, null, null, PatternConstants.POS_NOUN, PatternConstants.PTN_N,
					AnalysisOutput.SCORE_ANALYSIS);
			candidates.add(o);
		}

		fillSourceString(input, candidates);

		return candidates;
	}

	/**
	 * 조사의 첫음절부터 조사의 2음절이상에 사용될 수 있는 음절을 조사하여
	 * 가장 큰 조사를 찾는다.
	 * @param snipt
	 * @param jstart
	 * @return
	 * @throws MorphException
	 */
	private int findJosaEnd(String snipt, int jstart) throws MorphException {

		int jend = jstart;

		// [것을]이 명사를 이루는 경우는 없다.
		if(snipt.charAt(jstart - 1) == '것' && (snipt.charAt(jstart) == '을'))
			return jstart + 1;

		if(snipt.length() > jstart + 2 && snipt.charAt(jstart + 1) == '스') { // 사랑스러운, 자랑스러운 같은 경우르 처리함.
			char[] chrs = MorphRule.decompose(snipt.charAt(jstart + 2));

			if(chrs.length >= 2 && chrs[0] == 'ㄹ' && chrs[1] == 'ㅓ')
				return -1;
		}

		// 조사의 2음절로 사용될 수 마지막 음절을 찾는다.
		for(int i = jstart + 1; i < snipt.length(); i++) {
			char[] f = Dictionary.getSyllableFeature(snipt.charAt(i));
			if(f[SyllableRule.IDX_JOSA2] == '0')
				break;
			jend = i;
		}

		int start = jend;
		boolean hasJosa = false;
		for(int i = start; i >= jstart; i--) {
			String str = snipt.substring(jstart, i + 1);
			if(Dictionary.existJosa(str) && !findNounWithinStr(snipt, i, i + 2) && !isNounPart(snipt, jstart)) {
				jend = i;
				hasJosa = true;
				break;
			}
		}

		if(!hasJosa)
			return -1;

		return jend + 1;

	}

	/**
	 * 향후 계산이나 원 문자열을 보여주기 위해 source string 을 저장한다.
	 * @param source
	 * @param candidates
	 */
	private void fillSourceString(String source, List<AnalysisOutput> candidates) {

		for(AnalysisOutput o : candidates) {
			o.setSource(source);
		}

	}

	/**
	 * 목록의 1번지가 가장 큰 길이를 가진다.
	 * @param candidates
	 */
	private void appendSingleWord(List<AnalysisOutput> candidates) throws MorphException {

		if(candidates.size() == 0)
			return;

		String source = candidates.get(0).getSource();

		WordEntry entry = Dictionary.getWordExceptVerb(source);

		if(entry != null) {
			candidates.add(buildSingleOutput(entry));
		} else {

			if(candidates.get(0).getPatn() > PatternConstants.PTN_VM
					&& candidates.get(0).getPatn() <= PatternConstants.PTN_VMXMJ)
				return;

			if(source.length() < 5)
				return;

			AnalysisOutput o = new AnalysisOutput(source, null, null, PatternConstants.POS_NOUN,
					PatternConstants.PTN_N, AnalysisOutput.SCORE_ANALYSIS);
			o.setSource(source);
			morphAnal.confirmCNoun(o);
			if(o.getScore() == AnalysisOutput.SCORE_CORRECT)
				candidates.add(o);
		}
	}

	private void addSingleWord(String source, List<AnalysisOutput> candidates) throws MorphException {

		WordEntry entry = Dictionary.getWordExceptVerb(source);

		if(entry != null) {
			candidates.add(buildSingleOutput(entry));
		} else {
			AnalysisOutput o = new AnalysisOutput(source, null, null, PatternConstants.POS_NOUN,
					PatternConstants.PTN_N, AnalysisOutput.SCORE_ANALYSIS);
			o.setSource(source);
			morphAnal.confirmCNoun(o);
			candidates.add(o);
		}

		//		Collections.sort(candidates, new WSOuputComparator());

	}

	private List anlysisWithEomi(String snipt, int estart) throws MorphException {

		List<AnalysisOutput> candidates = new ArrayList();

		int eend = findEomiEnd(snipt, estart);

		// 동사앞에 명사분리
		int vstart = 0;
		for(int i = estart - 1; i >= 0; i--) {
			Iterator iter = Dictionary.findWithPrefix(snipt.substring(i, estart));
			if(iter.hasNext())
				vstart = i;
			else
				break;
		}

		if(snipt.length() > eend && Dictionary.findWithPrefix(snipt.substring(vstart, eend + 1)).hasNext())
			return candidates; // 다음음절까지 단어의 일부라면.. 분해를 안한다.

		String pvword = null;
		if(vstart != 0)
			pvword = snipt.substring(0, vstart);

		while(true) { // ㄹ,ㅁ,ㄴ 이기때문에 어미위치를 뒤로 잡았는데, 용언+어미의 형태가 아니라면.. 어구 끝을 하나 줄인다.
			String input = snipt.substring(vstart, eend);
			anlysisWithEomiDetail(input, candidates);
			if(candidates.size() == 0)
				break;
			if(("ㄹ".equals(candidates.get(0).getEomi()) || "ㅁ".equals(candidates.get(0).getEomi()) || "ㄴ"
					.equals(candidates.get(0).getEomi()))
					&& eend > estart + 1
					&& candidates.get(0).getPatn() != PatternConstants.PTN_VM
					&& candidates.get(0).getPatn() != PatternConstants.PTN_NSM) {
				eend--;
			} else if(pvword != null && candidates.get(0).getPatn() >= PatternConstants.PTN_VM && // 명사 + 용언 어구 중에.. 용언어구로 단어를 이루는 경우는 없다.
					candidates.get(0).getPatn() <= PatternConstants.PTN_VMXMJ && Dictionary.getWord(input) != null) {
				candidates.clear();
				break;
			} else if(pvword != null && VerbRule.verbSuffix(candidates.get(0).getStem())
					&& Dictionary.getNoun(pvword) != null) { // 명사 + 용언화 접미사 + 어미 처리
				candidates.clear();
				anlysisWithEomiDetail(snipt.substring(0, eend), candidates);
				pvword = null;
				break;
			} else {
				break;
			}
		}

		if(candidates.size() > 0 && pvword != null) {
			AnalysisOutput o = new AnalysisOutput(pvword, null, null, PatternConstants.POS_NOUN,
					PatternConstants.PTN_N, AnalysisOutput.SCORE_ANALYSIS);
			morphAnal.confirmCNoun(o);

			List<CompoundEntry> cnouns = o.getCNounList();
			if(cnouns.size() == 0) {
				boolean is = Dictionary.getWordExceptVerb(pvword) != null;
				cnouns.add(new CompoundEntry(pvword, 0, is));
			}

			for(AnalysisOutput candidate : candidates) {
				candidate.getCNounList().addAll(cnouns);
				candidate.getCNounList().add(new CompoundEntry(candidate.getStem(), 0, true));
				candidate.setStem(pvword + candidate.getStem()); // 이렇게 해야 WSOutput 에 복합명사 처리할 때 정상처리됨
			}

		}

		fillSourceString(snipt.substring(0, eend), candidates);

		return candidates;
	}

	private void anlysisWithEomiDetail(String input, List<AnalysisOutput> candidates) throws MorphException {

		boolean eomiFlag = true;

		int strlen = input.length();

		char ch = input.charAt(strlen - 1);
		char[] feature = Dictionary.getSyllableFeature(ch);

		if(feature[SyllableRule.IDX_YNPNA] == '1' || feature[SyllableRule.IDX_YNPLA] == '1'
				|| feature[SyllableRule.IDX_YNPMA] == '1')
			morphAnal.analysisWithEomi(input, "", candidates);

		for(int i = strlen - 1; i > 0; i--) {

			String stem = input.substring(0, i);
			String eomi = input.substring(i);

			feature = Dictionary.getSyllableFeature(eomi.charAt(0));

			if(eomiFlag) {
				morphAnal.analysisWithEomi(stem, eomi, candidates);
			}

			if(eomiFlag && feature[SyllableRule.IDX_EOMI2] == '0')
				eomiFlag = false;

			if(!eomiFlag)
				break;
		}

	}

	/**
	 * 어미의 첫음절부터 어미의 1음절이상에 사용될 수 있는 음절을 조사하여
	 * 가장 큰 조사를 찾는다.
	 * @param snipt
	 * @param jstart
	 * @return
	 * @throws MorphException
	 */
	private int findEomiEnd(String snipt, int estart) throws MorphException {

		int jend = 0;

		String tail = null;
		char[] chr = MorphRule.decompose(snipt.charAt(estart));
		if(chr.length == 3 && (chr[2] == 'ㄴ')) {
			tail = '은' + snipt.substring(estart + 1);
		} else if(chr.length == 3 && (chr[2] == 'ㄹ')) {
			tail = '을' + snipt.substring(estart + 1);
		} else if(chr.length == 3 && (chr[2] == 'ㅂ')) {
			tail = '습' + snipt.substring(estart + 1);
		} else {
			tail = snipt.substring(estart);
		}

		// 조사의 2음절로 사용될 수 마지막 음절을 찾는다.
		int start = 0;
		for(int i = 1; i < tail.length(); i++) {
			char[] f = Dictionary.getSyllableFeature(tail.charAt(i));
			if(f[SyllableRule.IDX_EOGAN] == '0')
				break;
			start = i;
		}

		for(int i = start; i > 0; i--) { // 찾을 수 없더라도 1음절은 반드시 반환해야 한다.
			String str = tail.substring(0, i + 1);
			char[] chrs = MorphRule.decompose(tail.charAt(i));
			if(Dictionary.existEomi(str)
					|| (i < 2 && chrs.length == 3 && (chrs[2] == 'ㄹ' || chrs[2] == 'ㅁ' || chrs[2] == 'ㄴ'))) { // ㅁ,ㄹ,ㄴ이 연속된 용언은 없다, 사전을 보고 확인을 해보자
				jend = i;
				break;
			}
		}

		return estart + jend + 1;

	}

	/**
	 * validation 후 후보가 될 가능성이 높은 최상위 것을 결과에 추가한다.
	 * 
	 * @param output
	 * @param candidates
	 * @param stack
	 */
	private int validationAndAppend(WSOutput output, List<AnalysisOutput> candidates, String input)
			throws MorphException {

		if(candidates.size() == 0)
			return 0;

		AnalysisOutput o = candidates.remove(0);
		AnalysisOutput po = output.getPhrases().size() > 0 ? output.getPhrases().get(output.getPhrases().size() - 1)
				: null;

		String ejend = o.getSource().substring(o.getStem().length());

		char[] chrs = po != null && po.getStem().length() > 0 ? MorphRule.decompose(po.getStem().charAt(
				po.getStem().length() - 1)) : null;
		String pjend = po != null && po.getStem().length() > 0 ? po.getSource().substring(po.getStem().length()) : null;

		char ja = 'x'; // 임의의 문자
		if(po != null
				&& (po.getPatn() == PatternConstants.PTN_VM || po.getPatn() == PatternConstants.PTN_VMCM || po
						.getPatn() == PatternConstants.PTN_VMXM)) {
			char[] chs = MorphRule.decompose(po.getEomi().charAt(po.getEomi().length() - 1));
			if(chs.length == 3)
				ja = chs[2];
			else if(chs.length == 1)
				ja = chs[0];
		}

		int nEnd = output.getLastEnd() + o.getSource().length();

		char[] f = nEnd < input.length() ? Dictionary.getSyllableFeature(input.charAt(nEnd)) : null;

		// 밥먹고 같은 경우가 가능하나.. 먹고는 명사가 아니다.
		if(po != null && po.getPatn() == PatternConstants.PTN_N && candidates.size() > 0
				&& o.getPatn() == PatternConstants.PTN_VM && candidates.get(0).getPatn() == PatternConstants.PTN_N) {
			o = candidates.remove(0);
		} else if(po != null && po.getPatn() >= PatternConstants.PTN_VM && candidates.size() > 0
				&& candidates.get(0).getPatn() == PatternConstants.PTN_N && (ja == 'ㄴ' || ja == 'ㄹ')) { // 다녀가ㄴ, 사,람(e) 로 분해 방지
			o = candidates.remove(0);
		}

		//=============================================
		if(o.getPos() == PatternConstants.POS_NOUN && MorphRule.hasVerbOnly(o.getStem())) {
			output.removeLast();
			return -1;
		} else if(nEnd < input.length() && f[SyllableRule.IDX_JOSA1] == '1'
				&& Dictionary.getNoun(o.getSource()) != null) {
			return -1;
		} else if(nEnd < input.length() && o.getScore() == AnalysisOutput.SCORE_ANALYSIS
				&& Dictionary.findWithPrefix(ejend + input.charAt(nEnd)).hasNext()) { // 루씬하ㄴ 글형태소분석기 방지
			return -1;
		} else if(po != null && po.getPatn() == PatternConstants.PTN_VM && "ㅁ".equals(po.getEomi())
				&& o.getStem().equals("하")) { // 다짐 합니다 로 분리되는 것 방지
			output.removeLast();
			return -1;
		} else if(po != null && po.getPatn() == PatternConstants.PTN_N && VerbRule.verbSuffix(o.getStem())
				&& !"있".equals(o.getStem())) { // 사랑받다, 사랑스러운을 처리, 그러나 있은 앞 단어와 결합하지 않는다.
			output.removeLast();
			return -1;
		} else {
			output.addPhrase(o);
		}

		return 1;
	}

	private AnalysisOutput buildSingleOutput(WordEntry entry) {

		char pos = PatternConstants.POS_NOUN;

		int ptn = PatternConstants.PTN_N;

		if(entry.getFeature(WordEntry.IDX_NOUN) == '0') {
			pos = PatternConstants.POS_AID;
			ptn = PatternConstants.PTN_AID;
		}

		AnalysisOutput o = new AnalysisOutput(entry.getWord(), null, null, pos, ptn, AnalysisOutput.SCORE_CORRECT);

		o.setSource(entry.getWord());

		return o;
	}

	private void analysisCompouns(List<AnalysisOutput> candidates) throws MorphException {

		// 복합명사 분해여부 결정하여 분해
		boolean changed = false;
		boolean correct = false;
		for(AnalysisOutput o : candidates) {

			if(o.getScore() == AnalysisOutput.SCORE_CORRECT) {
				if(o.getPatn() != PatternConstants.PTN_NJ)
					correct = true;
				// "활성화해"가 [활성화(N),하(t),어야(e)] 분석성공하였는데 [활성/화해]분해되는 것을 방지
				if("하".equals(o.getVsfx()))
					break;
				continue;
			}

			if(o.getPatn() <= PatternConstants.PTN_VM && o.getStem().length() > 2) {
				if(!(correct && o.getPatn() == PatternConstants.PTN_N))
					morphAnal.confirmCNoun(o);
				if(o.getScore() == AnalysisOutput.SCORE_CORRECT)
					changed = true;
			}
		}

	}

	/**
	 * 문자열에 
	 * @param str	분석하고자 하는 전체 문자열
	 * @param ws	문자열에서 명사를 찾는 시작위치
	 * @param es	문자열에서 명사를 찾는 끝 위치
	 * @return
	 * @throws MorphException
	 */
	private boolean findNounWithinStr(String str, int ws, int es) throws MorphException {

		if(str.length() < es)
			return false;

		for(int i = es; i < str.length(); i++) {
			char[] f = Dictionary.getSyllableFeature(str.charAt(i));
			if(i == str.length() || (f[SyllableRule.IDX_JOSA1] == '1')) {
				return (Dictionary.getWord(str.substring(ws, i)) != null);
			}
		}

		return false;
	}

	private boolean isNounPart(String str, int jstart) throws MorphException {

		if(true)
			return false;

		for(int i = jstart - 1; i >= 0; i--) {
			if(Dictionary.getWordExceptVerb(str.substring(i, jstart + 1)) != null)
				return true;

		}

		return false;

	}

	private void printCandidate(WSOutput output) {

		List<AnalysisOutput> os = output.getPhrases();
		for(AnalysisOutput o : os) {
			System.out.print(o.toString() + "(" + o.getScore() + ")| ");
		}
		System.out.println("<==");

	}
}
